package com.noahsloan.nutils.xml.serial;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import com.noahsloan.nutils.Utils;

/**
 * {@link #toDom(Object, Document, DomSerializerManager)}
 * 
 * @author noah
 * 
 */
public class AnnotatedObjectDomSerializer implements DomSerializer<Object> {

	/**
	 * Checks obj for {@link XmlElement}, {@link XmlAttribute},
	 * {@link XmlCollection}, and {@link XmlFollow} annotations and serializes
	 * appropriately.
	 * <p>
	 * Only annotated properties are serialized. If obj does not have the
	 * {@link XmlElement} annotation, an {@link IllegalArgumentException} will
	 * be thrown.
	 */
	@SuppressWarnings("unchecked")
	public Node toDom(Object obj, Document doc, DomSerializerManager manager) {
		if (obj == null) {
			return null;
		}
		Class<? extends Object> clazz = obj.getClass();

		// check the annotation
		XmlElement annotation = clazz.getAnnotation(XmlElement.class);
		if (annotation == null) {
			throw new IllegalArgumentException(clazz
					+ " is not annotated and does not have a DomSerializer.");
		}

		// create an element for the object
		Element element = doc.createElement(annotation.tagName());

		// get the property descriptors
		BeanInfo beanInfo;
		try {
			beanInfo = Introspector.getBeanInfo(clazz);
		} catch (IntrospectionException e) {
			throw new RuntimeException("Introspection failed on " + clazz, e);
		}

		// check each property for annotations
		for (PropertyDescriptor pd : beanInfo.getPropertyDescriptors()) {
			try {
				Method read = pd.getReadMethod();
				Object pValue = read.invoke(obj);

				if (pValue == null) {
					continue;
				}

				// see if it is an attribute
				XmlAttribute attrib = read.getAnnotation(XmlAttribute.class);

				if (attrib != null) {

					element.setAttribute(Utils.getValue(attrib.name(), pd
							.getName()), manager.serialize(pValue));
				} else {
					// if not, see if it is a nested element
					XmlFollow follow = read.getAnnotation(XmlFollow.class);
					if (follow != null) {
						element.appendChild(manager.nextToDom(pValue, doc));
					} else {
						for (Node child : tryCollection(doc, manager, pd,
								pValue)) {
							element.appendChild(child);
						}
					}
				}
			} catch (Exception e) {
				throw new RuntimeException("Error getting value of "
						+ pd.getName(), e);
			}
		}

		return element;
	}

	/**
	 * Creates nodes from the value, if it has the {@link XmlCollection}
	 * annotation.
	 * 
	 * @param doc
	 * @param manager
	 * @param pd
	 * @param pValue
	 * @return
	 * @throws Exception
	 */
	protected List<Node> tryCollection(Document doc,
			DomSerializerManager manager, PropertyDescriptor pd, Object pValue)
			throws Exception {
		XmlCollection collection = pd.getReadMethod().getAnnotation(
				XmlCollection.class);
		List<Node> nodes = new ArrayList<Node>();
		if (collection == null || pValue == null) {
			return nodes;
		}

		// get the items to be serialized
		Class<?> cType = pValue.getClass();
		Collection<?> items = null;
		if (Collection.class.isAssignableFrom(cType)) {
			items = (Collection<?>) pValue;
		} else if (Map.class.isAssignableFrom(cType)) {
			items = ((Map<?,?>) pValue).values();
		} else {
			throw new IllegalArgumentException(
					"Argument is not a Collection or a Map");
		}

		// create an element for each item
		String tag = collection.itemTag();
		for (Object item : items) {
			if (Utils.isEmpty(tag)) {
				nodes.add(manager.nextToDom(item, doc));
			} else {
				Node itemNode = doc.createElement(tag);
				itemNode.appendChild(manager.nextToDom(item, doc));
				nodes.add(itemNode);
			}
		}
		return nodes;
	}

}
